// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license.

#include "LODRecipe.h"
#include "LODRecipeRegistry.h"

#include "Engine/StaticMesh.h"
#include "Engine/SkeletalMesh.h"
#include "StaticMeshResources.h"
#include "Misc/MessageDialog.h"

#if WITH_EDITOR
#include "StaticMeshAttributes.h"
#include "SimplygonModule.h"
#include "SimplygonJobData.h"
#include "SimplygonDataConverter.h"
#include "SimplygonSettings.h"

#include "Rendering/SkeletalMeshModel.h"
#include "ClothingAsset.h"
#include "LODUtilities.h"

#include "Developer/MaterialUtilities/Public/MaterialUtilities.h"
#include "Developer/MeshMergeUtilities/Private/ProxyMaterialUtilities.h"

#include "AssetRegistryModule.h"
#include "AssetData.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Editor.h"
#include "Factories/FbxStaticMeshImportData.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "IStaticMeshEditor.h"
#include "MeshUtilitiesCommon.h"
#endif

DEFINE_LOG_CATEGORY_STATIC(LogLODRecipe, Verbose, All);
#define LOCTEXT_NAMESPACE "LODRecipe"

#if WITH_EDITOR
ULODRecipe::FNewAssetsCreatedEvent ULODRecipe::NewAssetsCreatedEvent;
#endif

struct LODRecipeHelper
{
	static void SyncAssets(const TArray<UObject*>& AssetsToSync)
	{
		if (AssetsToSync.Num())
		{
			FAssetRegistryModule& AssetRegistry = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
			for (UObject* asset : AssetsToSync)
			{
				AssetRegistry.AssetCreated(asset);
				GEditor->BroadcastObjectReimported(asset);
			
			}
		}
	}

	static bool IsPipelineCastingMaterials(Simplygon::spPipeline Pipeline)
	{
		Simplygon::spObjectCollection MaterialCasters = Pipeline->GetMaterialCasters();
		return !MaterialCasters.IsNull() && MaterialCasters->GetItemCount() > 0;
	}

	static bool IsPipelineCastingGeometryData(Simplygon::spPipeline Pipeline)
	{
		Simplygon::spObjectCollection MaterialCasters = Pipeline->GetMaterialCasters();
		for (Simplygon::rhandle CasterHandle = MaterialCasters->GetFirstItem();
			CasterHandle;
			CasterHandle = MaterialCasters->GetNextItem(CasterHandle))
		{
			auto GeometryCaster = Simplygon::spGeometryDataCaster::SafeCast(MaterialCasters.GetItemsObject(CasterHandle));
			if (GeometryCaster != nullptr)
				return true;
		}
		return false;
	}
};

//////////////////////////////////////////////////////////////////////////
// ULODAsset

class FScopedModifyLODRecipe
{
public:
	explicit FScopedModifyLODRecipe(ULODRecipe* RecipeToModify)
		: Recipe(RecipeToModify)
	{
		Recipe->Modify();
	}

	~FScopedModifyLODRecipe()
	{
		Recipe->PostEditChange();
		Recipe = nullptr;
	}

private:
	ULODRecipe* Recipe;
};

ULODRecipe::ULODRecipe(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
	, NumLODs(4)
	, NumStreamedLODs(-1)
{
	if(PerLODPipelineSettings.Num() != MAX_MESH_LOD_COUNT)
		PerLODPipelineSettings.SetNum(MAX_MESH_LOD_COUNT);

	if (PerLODMiscSettings.Num() != MAX_MESH_LOD_COUNT)
		PerLODMiscSettings.SetNum(MAX_MESH_LOD_COUNT);

	// "PostLoad is not called for newly-created objects" see PostLoad() comment in Object.h
	// Don't init settings when loading or duplicating existing recipes
	bool bIsDuplicatedAsset = HasAllFlags(RF_LoadCompleted | RF_WasLoaded);
	bool bIsLoadingExistingAsset = HasAllFlags(RF_NeedPostLoad);
	bool bInitializeDefaultRecipeSettings = !bIsLoadingExistingAsset && !bIsDuplicatedAsset;
	if (bInitializeDefaultRecipeSettings)
	{
		for (int32 LODIndex = 1; LODIndex < NumLODs; ++LODIndex)
		{
			UReductionPipeline* DefaultReduction = ObjectInitializer.CreateDefaultSubobject<UReductionPipeline>(this, *FString::Printf(TEXT("DefaultReductionPipeline%d"), LODIndex));
			DefaultReduction->Settings.ReductionSettings.ReductionTargetTriangleRatio = FMath::Pow(0.5f, float(LODIndex));
			PerLODPipelineSettings[LODIndex] = DefaultReduction;
		}
	}

	Key = FGuid::NewGuid();
}

#if WITH_EDITORONLY_DATA
void ULODRecipe::PostDuplicate(bool bDuplicateForPIE)
{
	StaticMeshes.Empty();
	SkeletalMeshes.Empty();
	Key = FGuid::NewGuid();

	Super::PostDuplicate(bDuplicateForPIE);
}
#endif

#if WITH_EDITOR
void ULODRecipe::PostLoad()
{
	if (PerLODPipelineSettings.Num() != MAX_MESH_LOD_COUNT)
		PerLODPipelineSettings.SetNum(MAX_MESH_LOD_COUNT);

	if (PerLODMiscSettings.Num() != MAX_MESH_LOD_COUNT)
		PerLODMiscSettings.SetNum(MAX_MESH_LOD_COUNT);

	Super::PostLoad();
}

void ULODRecipe::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);
}

void ULODRecipe::AddMesh(TSoftObjectPtr<UObject> Mesh)
{
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
	FAssetData MeshAssetData = AssetRegistryModule.Get().GetAssetByObjectPath(*Mesh.ToString());

	// Remove current LOD recipe from the mesh
	TSoftObjectPtr<ULODRecipe> OldLODRecipe = FLODRecipeRegistry::Get().FindLODRecipe(Mesh);
	OldLODRecipe.LoadSynchronous();
	if (OldLODRecipe.IsValid())
	{
		// Early return if it is the same recipe
		if (OldLODRecipe->GetKey() == GetKey())
			return;

		OldLODRecipe->RemoveMesh(Mesh);
	}

	bool bMeshAdded = false;
	if (MeshAssetData.AssetClass.ToString() == UStaticMesh::StaticClass()->GetName())
	{
		TSoftObjectPtr<UStaticMesh> StaticMesh(MeshAssetData.ToSoftObjectPath());
		Modify();
		StaticMeshes.AddUnique(StaticMesh);
		TriggerPropertyChanged(GET_MEMBER_NAME_CHECKED(ULODRecipe, StaticMeshes), EPropertyChangeType::ArrayAdd);
		bMeshAdded = true;
	}
	else if (MeshAssetData.AssetClass.ToString() == USkeletalMesh::StaticClass()->GetName())
	{
		TSoftObjectPtr<USkeletalMesh> SkeletalMesh(MeshAssetData.ToSoftObjectPath());
		Modify();
		SkeletalMeshes.AddUnique(SkeletalMesh);
		TriggerPropertyChanged(GET_MEMBER_NAME_CHECKED(ULODRecipe, SkeletalMeshes), EPropertyChangeType::ArrayAdd);
		bMeshAdded = true;
	}

	if (bMeshAdded)
	{	
		//Update registry
		FLODRecipeRegistry::Get().Add(Mesh, this);
	}
}

void ULODRecipe::RemoveMesh(TSoftObjectPtr<UObject> Mesh)
{
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
	FAssetData MeshAssetData = AssetRegistryModule.Get().GetAssetByObjectPath(*Mesh.ToString());

	bool bMeshRemoved = false;
	if (MeshAssetData.AssetClass.ToString() == UStaticMesh::StaticClass()->GetName())
	{
		TSoftObjectPtr<UStaticMesh> StaticMesh(MeshAssetData.ToSoftObjectPath());
		Modify();
		StaticMeshes.Remove(StaticMesh);
		TriggerPropertyChanged(GET_MEMBER_NAME_CHECKED(ULODRecipe, StaticMeshes), EPropertyChangeType::ArrayRemove);
		bMeshRemoved = true;
	}
	else if (MeshAssetData.AssetClass.ToString() == USkeletalMesh::StaticClass()->GetName())
	{
		TSoftObjectPtr<USkeletalMesh> SkeletalMesh(MeshAssetData.ToSoftObjectPath());
		Modify();
		SkeletalMeshes.Remove(SkeletalMesh);
		TriggerPropertyChanged(GET_MEMBER_NAME_CHECKED(ULODRecipe, SkeletalMeshes), EPropertyChangeType::ArrayRemove);
		bMeshRemoved = true;
	}

	if (bMeshRemoved)
	{
		//Update registry
		FLODRecipeRegistry::Get().Remove(Mesh);
	}
}

void ULODRecipe::RemoveAllStaticMeshes()
{
	TArray<TSoftObjectPtr<UStaticMesh>> StaticMeshesToRemove = GetStaticMeshes();
	for (TSoftObjectPtr<UStaticMesh> StaticMesh : StaticMeshesToRemove)
	{
		RemoveMesh(StaticMesh);
	}
	StaticMeshes.Empty();
}

void ULODRecipe::RemoveAllSkeletalMeshes()
{
	TArray<TSoftObjectPtr<UStaticMesh>> SkeletalMeshesToRemove = GetStaticMeshes();
	for (TSoftObjectPtr<UStaticMesh> SkeletalMesh : SkeletalMeshesToRemove)
	{
		RemoveMesh(SkeletalMesh);
	}
	SkeletalMeshes.Empty();
}

void ULODRecipe::RemoveAllMeshes()
{
	RemoveAllStaticMeshes();
	RemoveAllSkeletalMeshes();
}

TArray<TSoftObjectPtr<UStaticMesh>> ULODRecipe::GetStaticMeshes() const
{
	return StaticMeshes;
}

TArray<TSoftObjectPtr<USkeletalMesh>> ULODRecipe::GetSkeletalMeshes() const
{
	return SkeletalMeshes;
}

TArray<TSoftObjectPtr<UObject>> ULODRecipe::GetAllMeshes() const
{
	TArray<TSoftObjectPtr<UObject>> Result;
	Result.Reserve(StaticMeshes.Num() + SkeletalMeshes.Num());

	for (const auto& StaticMesh : StaticMeshes)
	{
		Result.Add(TSoftObjectPtr<UObject>(StaticMesh.ToSoftObjectPath()));
	}

	for (const auto& SkeletalMesh : SkeletalMeshes)
	{
		Result.Add(TSoftObjectPtr<UObject>(SkeletalMesh.ToSoftObjectPath()));
	}

	return Result;
}

const TArray<USimplygonPipeline*>& ULODRecipe::GetPipelines() const
{
	return PerLODPipelineSettings;
}

Simplygon::spPipeline ULODRecipe::CreatePipeline(Simplygon::ISimplygon* SDK, FSimplygonFlattenMaterialSettings& OutFlattenMaterialSettings) const
{
	OutFlattenMaterialSettings.Reset();

	// Create a pass through pipeline so that we can utilize per-LOD distribution
	Simplygon::spPassthroughPipeline Pipeline = SDK->CreatePassthroughPipeline();

	for (int32 LODIndex = 0; LODIndex < NumLODs; ++LODIndex)
	{
		if (LODIndex == 0)
			continue;

		// Add a dummy PassthroughPipeline of no pipeline has been selected.
		// This is later handled in the complete method after Simplygon is done.
		if (!PerLODPipelineSettings[LODIndex])
		{
			Pipeline->AddCascadedPipeline(SDK->CreatePassthroughPipeline());
			continue;
		}

		FString ErrorMsg;
		if (!PerLODPipelineSettings[LODIndex]->HasValidSettings(ErrorMsg))
		{
			ErrorMsg = "Generation of LOD " + FString::FromInt(LODIndex) + " failed. " + ErrorMsg;
			UE_LOG(LogLODRecipe, Error, TEXT("%s"), *ErrorMsg);
			FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(ErrorMsg));

			Pipeline->AddCascadedPipeline(SDK->CreatePassthroughPipeline());
			continue;
		}

		Simplygon::spPipeline LODPipeline = PerLODPipelineSettings[LODIndex]->CreatePipeline(SDK);

		//applies transform to update up and direction vector from UE to Simplygon
		FSimplygonDataConverter::UpdateVectorsInPipeline( LODPipeline );
				
		FSimplygonFlattenMaterialSettings LODFlatteningSettings(LODPipeline);
		OutFlattenMaterialSettings.Concatenate(LODFlatteningSettings);
		OutFlattenMaterialSettings.ChartAggregatorSettings = PerLODMiscSettings[LODIndex].ChartAggregatorSettings;

		Pipeline->AddCascadedPipeline(LODPipeline);
	}

	return Pipeline;
}

void ULODRecipe::AllowMorphTargetsInLOD(const bool bAllowMorphTargets, const int32 LODIndex)
{
	if (PerLODMiscSettings.IsValidIndex(LODIndex))
	{
		PerLODMiscSettings[LODIndex].bApplyMorphTargets = bAllowMorphTargets;
	}
}

bool ULODRecipe::IsMorphTargetsAllowedInLOD(const int32 LODIndex) const
{
	if (PerLODMiscSettings.IsValidIndex(LODIndex))
	{
		return PerLODMiscSettings[LODIndex].bApplyMorphTargets;
	}

	return false;
}

bool ULODRecipe::IsMorphTargetsAllowed() const
{
	bool bShouldApplyMorphTargets = false;
	for (const auto& MiscSettings : PerLODMiscSettings)
	{
		bShouldApplyMorphTargets |= MiscSettings.bApplyMorphTargets;
	}
	return bShouldApplyMorphTargets;
}

bool ULODRecipe::IsMorphTargetRemovedInLOD(const UMorphTarget* MorphTarget, const int32 LODIndex) const
{
	TSoftObjectPtr<USkeletalMesh> SkeletalMesh = MorphTarget->BaseSkelMesh;
	const FMorphTargetsToRemove* MorphTargetsToRemove = PerLODMiscSettings[LODIndex].MorphTargetsToRemove.Find(SkeletalMesh);
	
	return (MorphTargetsToRemove != nullptr && MorphTargetsToRemove->MorphTargets.Contains(MorphTarget));
}

void ULODRecipe::RemoveMorphTargetInLOD(const UMorphTarget* MorphTarget, const int32 LODIndex)
{
	if (PerLODMiscSettings.IsValidIndex(LODIndex))
	{
		TSoftObjectPtr<USkeletalMesh> SkeletalMesh = MorphTarget->BaseSkelMesh;
		FMorphTargetsToRemove& MorphTargetsToRemove = PerLODMiscSettings[LODIndex].MorphTargetsToRemove.FindOrAdd(SkeletalMesh);
		
		if (!MorphTargetsToRemove.MorphTargets.Contains(MorphTarget))
			MorphTargetsToRemove.MorphTargets.Add(MorphTarget);			
	}
}

void ULODRecipe::IncludeMorphTargetInLOD(const UMorphTarget* MorphTarget, const int32 LODIndex)
{
	if (PerLODMiscSettings.IsValidIndex(LODIndex))
	{
		TSoftObjectPtr<USkeletalMesh> SkeletalMesh = MorphTarget->BaseSkelMesh;
		FMorphTargetsToRemove* MorphTargetsToRemove = PerLODMiscSettings[LODIndex].MorphTargetsToRemove.Find(SkeletalMesh);

		if (IsMorphTargetRemovedInLOD(MorphTarget, LODIndex))
		{
			MorphTargetsToRemove->MorphTargets.Remove(MorphTarget);
			MorphTargetsToRemove->MorphTargets.Compact();
		}
	}
}

static FString GetBakedMaterialName(UObject* Mesh, int32 LODIndex, bool bUseOldTempalte = false)
{
	// Adding a Sg prefix in the name to add more uniqueness
	//Compute path hash and take first 12 chars.
	/// Note this would still allow for collision. However should be less likely to occur.
	FString MeshPath = Mesh->GetPathName();
	FString PathHash = FMD5::HashAnsiString(*MeshPath);
	PathHash.LeftChopInline(12);
	if( bUseOldTempalte )
		return FString::Printf( TEXT( "%s_SgBakedMaterial_LOD%d" ), *Mesh->GetName(), LODIndex );
	return FString::Printf(TEXT("%s_%s_SgBakedMaterial_LOD%d"), *Mesh->GetName(), *PathHash, LODIndex);
}

static void RemoveMaterialAndTextures(UMaterialInterface* Material)
{
	TArray<UTexture*> Textures;
	Material->GetUsedTextures(Textures, EMaterialQualityLevel::Num, true, ERHIFeatureLevel::Num, true);
	TArray<UObject*> AssetsToDelete = { Material };
	AssetsToDelete.Append(Textures);
	for (UObject* OldAssetObject : AssetsToDelete)
	{
		if (OldAssetObject->HasAnyFlags(RF_Standalone))
		{
			if (OldAssetObject->IsA<UTexture>() ||
				OldAssetObject->IsA<UMaterialInterface>())
			{
				OldAssetObject->ClearFlags(RF_Standalone);
			}
		}
		OldAssetObject = nullptr;
	}
	CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
}

static void ResetMaterialsAndSectionInfo( UStaticMesh* StaticMesh, const TArray<bool>& PerLODUseImportedLOD )
{
	FMeshSectionInfoMap& SectionInfo = StaticMesh->GetSectionInfoMap();

	auto& StaticMaterials = StaticMesh->GetStaticMaterials();
	// Note: We use MAX_MESH_LOD_COUNT to remove stale material if the array size shrinks after processing with a material bake.
	for( int32 LODIndex = 0; LODIndex < MAX_MESH_LOD_COUNT; ++LODIndex )
	{
		// SG-TODO: Allow LOD0 reductions?
		if( LODIndex == 0 )
			continue;

		if( LODIndex < PerLODUseImportedLOD.Num() && PerLODUseImportedLOD[ LODIndex ] )
			continue;

		FString BakedMaterialName = GetBakedMaterialName( StaticMesh, LODIndex );
		FString OldBakedMaterialName = GetBakedMaterialName( StaticMesh, LODIndex, true );

		TArray<UMaterialInterface*> MaterailsToCleanup;

		// clean up unwanted materials mainly simplygon baked materials and null materials
		StaticMaterials.RemoveAll( [ & ]( const FStaticMaterial& InMaterial ) {
			// do not remove "null"-materials as it can result in material index offset,
			// in some cases even out-of-bounds when the asset is accessed after writeback.
			if( !InMaterial.MaterialInterface )
				return false;

			bool found = InMaterial.MaterialInterface->GetName().Contains( BakedMaterialName ) ||
			             InMaterial.MaterialInterface->GetName().Contains( OldBakedMaterialName );
			if( found )
				MaterailsToCleanup.Add( InMaterial.MaterialInterface );
			return found;
		} );

		for( auto Material : MaterailsToCleanup )
		{
			RemoveMaterialAndTextures( Material );
		}

		// Remove all sections from LOD
		int32 NumSectionsInLOD = SectionInfo.GetSectionNumber( LODIndex );
		for( int32 SectionIndex = 0; SectionIndex < NumSectionsInLOD; ++SectionIndex )
		{
			SectionInfo.Remove( LODIndex, SectionIndex );
		}
	}
	StaticMesh->SetStaticMaterials( StaticMaterials );
	SectionInfo.Map.Compact();
}

static void ResetMaterialsAndSectionInfo( USkeletalMesh* SkeletalMesh, const TArray<FLODRecipeMiscSettings>& PerLODMiscSettings )
{
	for( int32 LODIndex = 0; LODIndex < PerLODMiscSettings.Num(); ++LODIndex )
	{
		// SG-TODO: Allow LOD0 reductions?
		if( LODIndex == 0 )
			continue;

		if( PerLODMiscSettings[ LODIndex ].bUseImportedLOD )
			continue;

		FString BakedMaterialName = GetBakedMaterialName( SkeletalMesh, LODIndex );
		FString OldBakedMaterialName = GetBakedMaterialName( SkeletalMesh, LODIndex, true );

		TArray<UMaterialInterface*> MaterailsToCleanup;
		auto Materials = SkeletalMesh->GetMaterials();

		// clean up unwanted materials mainly simplygon baked materials and null materials
		Materials.RemoveAll( [ & ]( const FSkeletalMaterial& InMaterial ) {
			if( !InMaterial.MaterialInterface )
				return true;

			bool found = InMaterial.MaterialInterface->GetName().Contains( BakedMaterialName ) ||
			             InMaterial.MaterialInterface->GetName().Contains( OldBakedMaterialName );
			if( found )
				MaterailsToCleanup.Add( InMaterial.MaterialInterface );
			return found;
		} );

		for( auto Material : MaterailsToCleanup )
		{
			RemoveMaterialAndTextures( Material );
		}

		// Remove all sections from LOD
		FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo( LODIndex );
		if( LODInfo )
			LODInfo->LODMaterialMap.Empty();
	}
}


void ULODRecipe::ApplyBakedMaterials(UStaticMesh* StaticMesh, const int32 LODIndex, FFlattenMaterial& BakedMaterial, const Simplygon::spPipeline& Pipeline, const TArray<int32>& MaterialIndices, const TArray<uint32> BakedUVSets)
{
	FString BakedMaterialName = GetBakedMaterialName(StaticMesh, LODIndex);
	TArray<UObject*> AssetsToSync;

	// pick the base material template
	FString MaterialTemplatePath = FSimplygonDataConverter::GetPipelineBakedMaterialBaseTemplatePath( Pipeline, LODIndex-1 );

	bool isFlipbookMaterial = FSimplygonDataConverter::IsCascadedPipelineOfType<Simplygon::spFlipbookPipeline>( Pipeline, LODIndex-1 );
	bool isImposterMaterial = FSimplygonDataConverter::IsCascadedPipelineOfType<Simplygon::spBillboardCloudVegetationPipeline>( Pipeline, LODIndex - 1 ) ||
	                          FSimplygonDataConverter::IsCascadedPipelineOfType<Simplygon::spBillboardCloudPipeline>( Pipeline , LODIndex - 1 );

	//Get the correct count of source material
    int SourceMaterialCount = 0;
    for (const FStaticMaterial& Mat : StaticMesh->GetStaticMaterials()) 
	{
        if(!Mat.MaterialSlotName.ToString().Contains("SgBakedMaterial")) 
		{
            SourceMaterialCount++;
		}
    }
	

	bool HasSourceMaterials = MaterialIndices.Num() > 1;
	
	// Create baked materials and add to static mesh
	int32 NumOutputMaterials = 1;
	for (int32 OutputMaterialIndex = 0; OutputMaterialIndex < NumOutputMaterials; ++OutputMaterialIndex)
	{
		FFlattenMaterial& OutFlattenMaterial = BakedMaterial;
		FString SimplygonBakedMaterialsDir = FPaths::Combine(TEXT("/Game"),TEXT("Simplygon"), TEXT("LODRecipe"), TEXT("Materials"));
		FString AbsoluteBakedMaterialName = FPaths::Combine(SimplygonBakedMaterialsDir, BakedMaterialName + FString::Printf(TEXT("_%d"), OutputMaterialIndex));

		//FMaterialUtilities::ResizeFlattenMaterial(OutFlattenMaterial, InCompleteData.ReductionSettings.MaterialProxySettings);

		// Optimize flattened material
		//FMaterialUtilities::OptimizeFlattenMaterial(OutFlattenMaterial);

		const FString AssetBaseName = FPackageName::GetShortName(AbsoluteBakedMaterialName);
		const FString AssetBasePath = FPackageName::GetLongPackagePath(AbsoluteBakedMaterialName) + TEXT("/");
		FSimplygonFlattenMaterialSettings FlattenMaterialSettings(Pipeline);
		FlattenMaterialSettings.BlendMode = BakedMaterial.BlendMode;
		UMaterialInterface* BaseMaterial = LoadObject<UMaterialInterface>( NULL, *MaterialTemplatePath, NULL, LOAD_None, NULL );
		UMaterialInstanceConstant* ProxyMaterial = ProxyMaterialUtilities::CreateProxyMaterialInstance(NULL, FlattenMaterialSettings, BaseMaterial, OutFlattenMaterial, AssetBasePath, AssetBaseName, AssetsToSync);

		for (UObject* Object : AssetsToSync)
		{
			if (Object->GetName().EndsWith(TEXT("_Emissive")))
			{
				UTexture2D* EmissveTexture = Cast<UTexture2D>(Object);
				EmissveTexture->CompressionSettings = TC_HDR_Compressed;
			}
		}
		// NOTE: If default UV Channel used for the merged UV or trunk and foliage is not UV1 enabled the following code block.
//#ifdef ENABLE_UV1
		FStaticParameterSet StaticParameterSet = ProxyMaterial->GetStaticParameters();
		if( isImposterMaterial )
		{
			uint32 BakedUVSet = 0;
			if (BakedUVSets.Num() > 0)
				BakedUVSet = BakedUVSets[0];
			FStaticSwitchParameter SwitchParameter;
			SwitchParameter.ParameterInfo.Name = *FString::Printf(TEXT("UseUV%d"),BakedUVSet); // TEXT( "UseUV1" );
			SwitchParameter.Value = true;
			SwitchParameter.bOverride = true;
			StaticParameterSet.StaticSwitchParameters.Add( SwitchParameter );

			// This requires that the base template for imposter material has logic setup was highly experimental
#ifdef USE_CLAMP_NODE_IN_BASE_TEMPLATE
			ProxyMaterial->SetScalarParameterValueEditorOnly( FMaterialParameterInfo( "ClampMaskToConst" ), 0.0f );
			ProxyMaterial->SetScalarParameterValueEditorOnly( FMaterialParameterInfo( "MaskCutOffConst" ), 0.5f );
			ProxyMaterial->BasePropertyOverrides.bOverride_OpacityMaskClipValue = true;
			ProxyMaterial->BasePropertyOverrides.OpacityMaskClipValue = 0.3333f;
#endif
			ProxyMaterial->UpdateStaticPermutation( StaticParameterSet );
		}
//#endif

		if (isFlipbookMaterial) {

			Simplygon::spFlipbookPipeline FlipbookPipeline;
			float NumView = 0.f;
			if (Pipeline->GetCascadedPipelineCount() == 0) {
				FlipbookPipeline = Simplygon::spFlipbookPipeline::SafeCast( Pipeline );
			}
			else
			{
				FlipbookPipeline = Simplygon::spFlipbookPipeline::SafeCast( Pipeline->GetCascadedPipelineByIndex( LODIndex - 1 ) );
			}

			if (FlipbookPipeline) {
				NumView = float(FlipbookPipeline->GetFlipbookSettings()->GetNumberOfViews());
			}
			

			ProxyMaterial->SetScalarParameterValueEditorOnly( FMaterialParameterInfo( "FlipbookViewsConst" ), NumView );
		
		}

		//NOTE: Currently assuming that baked material is the last slot and assuming all source material are returned. 
		int32 SlotIndex = OutputMaterialIndex;
		FMeshSectionInfo Info = StaticMesh->GetSectionInfoMap().Get(LODIndex, SlotIndex);
		if (LODIndex == 0)
		{
			check(Info.MaterialIndex == SlotIndex);
			check(StaticMesh->GetStaticMaterials().IsValidIndex(SlotIndex));
			StaticMesh->GetStaticMaterials()[SlotIndex] = ProxyMaterial;
		}
		else
		{
            if (HasSourceMaterials && isImposterMaterial)
			{
				
				for (auto MaterialIndex : MaterialIndices) {
					 
					Info = StaticMesh->GetSectionInfoMap().Get(LODIndex, SlotIndex);
                    if (MaterialIndex == SourceMaterialCount)
					{
						int32 NewMaterialIndex = StaticMesh->GetStaticMaterials().Add(ProxyMaterial);
						Info.MaterialIndex = NewMaterialIndex;
						StaticMesh->GetSectionInfoMap().Set(LODIndex, SlotIndex, Info);
					}
					else
					{
						Info.MaterialIndex = MaterialIndex;
						StaticMesh->GetSectionInfoMap().Set(LODIndex, SlotIndex, Info);
					}
						
					
					SlotIndex += 1;
				}
				
			}
			else
			{
				int32 NewMaterialIndex = StaticMesh->GetStaticMaterials().Add(ProxyMaterial);
				Info.MaterialIndex = NewMaterialIndex;
				StaticMesh->GetSectionInfoMap().Set(LODIndex, SlotIndex, Info);
			}
		}
	}

	

	LODRecipeHelper::SyncAssets(AssetsToSync);

	NewAssetsCreatedEvent.Broadcast(&AssetsToSync);
}

void ULODRecipe::ApplyBakedMaterials(USkeletalMesh* SkeletalMesh, const int32 LODIndex, FFlattenMaterial& BakedMaterial, const Simplygon::spPipeline& Pipeline)
{
	FString BakedMaterialName = GetBakedMaterialName(SkeletalMesh, LODIndex);

	// Prepare place for baked materials
	int32 NumOutputMaterials = 1;
	FSkeletalMeshLODInfo* NewLODInfo = SkeletalMesh->GetLODInfo(LODIndex);
	NewLODInfo->LODMaterialMap.SetNumZeroed(NumOutputMaterials);

	TArray<UObject*> AssetsToSync;

	for (int32 OutMaterialIndex = 0; OutMaterialIndex < NumOutputMaterials; OutMaterialIndex++)
	{
		FFlattenMaterial& OutFlattenMaterial = BakedMaterial;
		FString SimplygonBakedMaterialsDir = FPaths::Combine(TEXT("/Game"), TEXT("Simplygon"), TEXT("LODRecipe"), TEXT("Materials"));
		FString AbsoluteBakedMaterialName = FPaths::Combine(SimplygonBakedMaterialsDir, BakedMaterialName + FString::Printf(TEXT("_%d"), OutMaterialIndex));

		const FString AssetBaseName = FPackageName::GetShortName(AbsoluteBakedMaterialName);
		const FString AssetBasePath = FPackageName::GetLongPackagePath(AbsoluteBakedMaterialName) + TEXT("/");
		FSimplygonFlattenMaterialSettings FlattenMaterialSettings(Pipeline);
		FlattenMaterialSettings.BlendMode = BakedMaterial.BlendMode;
		UMaterialInterface* BaseMaterial = LoadObject<UMaterialInterface>(NULL, TEXT("/Engine/EngineMaterials/BaseFlattenMaterial.BaseFlattenMaterial"), NULL, LOAD_None, NULL);
		UMaterialInstanceConstant* ProxyMaterial = ProxyMaterialUtilities::CreateProxyMaterialInstance(NULL, FlattenMaterialSettings, BaseMaterial, OutFlattenMaterial, AssetBasePath, AssetBaseName, AssetsToSync);

		//@SG-TODO: Is this necessary? Disabled for now.
#if 0
		// flag the property (Materials) we're modifying so that not all of the object is rebuilt.
		FProperty* ChangedProperty = NULL;
		ChangedProperty = USkeletalMesh::StaticClass()->FindPropertyByName(GET_MEMBER_NAME_CHECKED(USkeletalMesh, Materials));
		check(ChangedProperty);
		SkeletalMesh->PreEditChange(ChangedProperty);
#endif

		// store baked material to Materials and LODInfo	
		int32 NewMaterialIndex = SkeletalMesh->GetMaterials().Add(FSkeletalMaterial(ProxyMaterial));
		NewLODInfo->LODMaterialMap[OutMaterialIndex] = NewMaterialIndex;
	}

	LODRecipeHelper::SyncAssets(AssetsToSync);

	NewAssetsCreatedEvent.Broadcast(&AssetsToSync);
}

void ULODRecipe::TriggerPropertyChanged(const FName PropertyName, EPropertyChangeType::Type PropertyChangeType)
{
	FProperty* Property = StaticClass()->FindPropertyByName(PropertyName);
	if (Property)
	{
		FPropertyChangedEvent PropertyChanged(Property, PropertyChangeType);
		PostEditChangeProperty(PropertyChanged);
	}
}

void ULODRecipe::ProcessCompleted(UStaticMesh* StaticMesh, const FSimplygonLODRecipeJobData& LODRecipeJobData, const Simplygon::spPipeline& Pipeline, const Simplygon::spScene& CascadedScene)
{
	check(IsInGameThread())

	UE_LOG(LogLODRecipe, Display, TEXT("Simplygon pipeline complete, finalizing %s"), *StaticMesh->GetName());

	TArray<FMeshDescription> ReducedMeshDescriptions;
	ReducedMeshDescriptions.SetNum(NumLODs);

	TMap<int32, FFlattenMaterial> PerLODBakedMaterials;
	PerLODBakedMaterials.Empty();

	TArray<bool> PerLODUseImportedLOD;
	PerLODUseImportedLOD.Init(false, NumLODs);

	// Create a lookup map for generated LOD slot name to source material element
	const TMap<FName, int32> SlotNameToMaterialElement = [&StaticMesh, &LODRecipeJobData]()
	{
		TMap<FName, int32> SlotNameToMaterialElement;
		TArray<FName> MaterialSlotNames;
		LODRecipeJobData.MaterialIndexToSlotName.GenerateValueArray(MaterialSlotNames);
		for (const FName& SlotName : MaterialSlotNames)
		{
			int32 MaterialElementIndex = StaticMesh->GetMaterialIndexFromImportedMaterialSlotName(SlotName);
			SlotNameToMaterialElement.Add(SlotName, MaterialElementIndex);
		}

		return SlotNameToMaterialElement;
	}();

	// Create a lookup map for LOD0's slot name to section index
	const TMap<FName, int32> LOD0SlotNameToSectionIndex = [&StaticMesh]()
	{
		if(StaticMesh->GetStaticMaterials().Num() <= 0)
			return TMap<FName, int32>();

		TMap<FName, int32> SlotNameToSectionIndex;
		for (int32 LOD0SectionIndex = 0; LOD0SectionIndex < StaticMesh->GetSectionInfoMap().GetSectionNumber(0); ++LOD0SectionIndex)
		{
			const FMeshSectionInfo& LOD0Section = StaticMesh->GetSectionInfoMap().Get(0, LOD0SectionIndex);
			SlotNameToSectionIndex.Add(StaticMesh->GetStaticMaterials()[LOD0Section.MaterialIndex].ImportedMaterialSlotName, LOD0SectionIndex);
		}

		return SlotNameToSectionIndex;
	}();

	//FStaticMeshSourceModel& LODModel = StaticMesh->AddSourceModel();
	//FMeshDescription* MeshDescription = StaticMesh->CreateMeshDescription(LODIndex);
	//FStaticMeshAttributes Attributes(*MeshDescription);
	
	// Per LOD Section indices gathered from GeometryData's unique material indices
	// Needed in order to index sections from LOD 0 to the new LOD X.
	TMap<int32, TArray<int32>> PerLODMaterialIndices;

	auto CopyExistingMeshDescription = [&](const int32 SrcLODIndex, const int32 DestLODIndex)
	{
		FStaticMeshAttributes(ReducedMeshDescriptions[DestLODIndex]).Register();
		ReducedMeshDescriptions[DestLODIndex] = *StaticMesh->GetMeshDescription(SrcLODIndex);
	};

	// Setup per lod mapping between polygroup id and material index
	TArray<TMap<FPolygonGroupID, int32>> PerLODPolyGroupToMaterialIndexMapping;
	PerLODPolyGroupToMaterialIndexMapping.AddDefaulted(NumLODs);

	//Contains new uv sets used by baked texture
	TArray<TArray<uint32>> PerLODBakedUVSets;
	PerLODBakedUVSets.AddDefaulted(NumLODs);
	for (int32 LODIndex = 0; LODIndex < NumLODs; ++LODIndex)
	{
		//SG-TODO: Allow LOD0 reductions? If so change LODIndex - 1 below
		if (LODIndex == 0)
			continue;

		Simplygon::spPipeline LODPipeline = Pipeline->GetCascadedPipelineByIndex(LODIndex - 1);

		// Only support imported LODs and ignore generated LODs (native reduction interface).
		if (PerLODMiscSettings[LODIndex].bUseImportedLOD && 
			StaticMesh->IsSourceModelValid(LODIndex) && 
			StaticMesh->GetSourceModel(LODIndex).MeshDescriptionBulkData.IsValid())
		{
			PerLODUseImportedLOD[LODIndex] = true;
			continue;
		}

		// Fall back to LOD 0 if a PassthroughPipeline is found.
		// See ULODRecipe::CreatePipeline for more info about the logic
		if ( Simplygon::spPassthroughPipeline::SafeCast(LODPipeline) )
		{
			CopyExistingMeshDescription(0, LODIndex);
			continue;
		}

		Simplygon::spScene Scene = LODPipeline->GetProcessedScene();
		if (Scene.IsNull())
		{
			Scene = CascadedScene->GetCascadedSceneForIndex(LODIndex - 1);
			if (Scene.IsNull())
			{
				UE_LOG(LogLODRecipe, Error, TEXT("LOD recipe processing failed for %s"), *StaticMesh->GetFName().ToString());
				CopyExistingMeshDescription(0, LODIndex);
				continue;
			}
		}

		Simplygon::spSceneNode RootNode = Scene->GetRootNode();
		if (!RootNode->GetChildCount())
		{
			UE_LOG(LogLODRecipe, Error, TEXT("LOD recipe processing failed for %s"), *StaticMesh->GetFName().ToString());
			CopyExistingMeshDescription(0, LODIndex);
			continue;
		}

		Simplygon::spSceneMesh ReducedMesh =
			Simplygon::spSceneMesh::SafeCast(Scene->GetRootNode()->GetChild(0));
		if (!ReducedMesh)
		{
			CopyExistingMeshDescription(0, LODIndex);
			continue;
		}

		// Convert to FlattenMaterial structure
		const bool bHasBakedMaterials = LODRecipeHelper::IsPipelineCastingMaterials(LODPipeline);
		if (bHasBakedMaterials)
		{
			FFlattenMaterial BakedMaterial;
			FSimplygonDataConverter::CreateFlattenMaterial(Scene, BakedMaterial);
			BakedMaterial.FillAlphaValues(255);
			PerLODBakedMaterials.Add(LODIndex, BakedMaterial);
		}

		// Create Geometry Data textures
		if (LODRecipeHelper::IsPipelineCastingGeometryData(LODPipeline))
		{
			TArray<UObject*> AssetsToSync;
			FSimplygonDataConverter::CreateGeometryDataTextures(Scene, LODPipeline, GetBakedMaterialName(StaticMesh, LODIndex), AssetsToSync);
			LODRecipeHelper::SyncAssets(AssetsToSync);
			NewAssetsCreatedEvent.Broadcast(&AssetsToSync);
		}

		// Convert to Mesh description
		FStaticMeshAttributes(ReducedMeshDescriptions[LODIndex]).Register();

		auto ProcessedGeometry = ReducedMesh->GetGeometry();
		
		//NOTE: Since Billboard Cloud Vegetation Pipeline can output multiple meshes.
		//Get new combined geometry instead. 
		auto BillboardCloudVegetationPipeline = Simplygon::spBillboardCloudVegetationPipeline::SafeCast( LODPipeline );
		if( BillboardCloudVegetationPipeline && BillboardCloudVegetationPipeline->GetBillboardCloudSettings()->GetFoliageSettings()->GetSeparateTrunkAndFoliage())
		{
			auto setId = Scene->SelectNodes( "ISceneMesh" );
			auto selectedMeshes = Scene->GetSelectionSetTable()->GetSelectionSet( setId );
			UE_LOG( LogLODRecipe, Display, TEXT( "Simplygon pipeline selected mesh count: %d" ), selectedMeshes->GetItemCount() );
			
			Simplygon::ISimplygon* SimplygonAPI = ISimplygonModule::Get().GetSDK( true );
			if( !SimplygonAPI )
			{
				UE_LOG( LogLODRecipe, Error, TEXT( "Failed to get handle to SimplygonAPI" ) );
				return;
			}

			auto MergedGeometry = SimplygonAPI->CreateGeometryData();
			{
				auto mesh_selection_set = Scene->SelectNodes( "ISceneMesh" );
				auto mesh_set = Scene->GetSelectionSetTable()->GetSelectionSet( mesh_selection_set );
				for( auto index = 0U; index < mesh_set->GetItemCount(); ++index )
				{
					auto node_id = mesh_set->GetItem( index );
					auto MeshNode = Simplygon::spSceneMesh::SafeCast( Scene->GetNodeByGUID( node_id ) );
					MergedGeometry->AppendGeometry( MeshNode->GetGeometry() );
				}
			}

			//always keep UV0			
			for( auto UVIndex = 1U; UVIndex < MAX_STATIC_TEXCOORDS; ++UVIndex )
			{
				if(!LODRecipeJobData.LODRecipe->PerLODMiscSettings[ LODIndex ].UVsToKeep[UVIndex])
				{
					if(MergedGeometry->GetTexCoords(UVIndex))
						MergedGeometry->RemoveTexCoords( UVIndex );
				}
			}
		
			ProcessedGeometry = MergedGeometry;
		}

		//SG-TODO: Refactor material ids usage - it is only utilized for billboard cloud but is affecting all pipelines
		{
			TArray<int32>& MaterialIndices = PerLODMaterialIndices.Add( LODIndex );
			TMap<int32, FName> DummyMap;
			SetupPerLODMaterialIds(Scene, DummyMap, ProcessedGeometry, MaterialIndices);
		}

		FSimplygonDataConverter::CreateMeshDescriptionFromGeometry(ProcessedGeometry, LODRecipeJobData.MaterialIndexToSlotName, ReducedMeshDescriptions[LODIndex], PerLODPolyGroupToMaterialIndexMapping[LODIndex], PerLODBakedUVSets[LODIndex], bHasBakedMaterials, !BillboardCloudVegetationPipeline.IsNull());
	}

	//Clear the LODGroup
	{
		StaticMesh->SetLODGroup(NAME_None, false);
		//Make sure the import data point on LOD Group None
		UFbxStaticMeshImportData* ImportData = Cast<UFbxStaticMeshImportData>(StaticMesh->AssetImportData);
		if (ImportData != nullptr)
		{
			ImportData->StaticMeshLODGroup = NAME_None;
		}
	}

	ResetMaterialsAndSectionInfo(StaticMesh, PerLODUseImportedLOD);
	
	/** Put LODs into StaticMesh */
	StaticMesh->SetNumSourceModels(NumLODs);
	for (int32 LODIndex = 0; LODIndex < ReducedMeshDescriptions.Num(); ++LODIndex)
	{
		//SG-TODO: Allow LOD0 reductions?
		if (LODIndex == 0)
			continue;

		if (PerLODUseImportedLOD[LODIndex])
			continue;
		
		Simplygon::spPipeline LODPipeline = Pipeline->GetCascadedPipelineByIndex( LODIndex - 1 );
		auto BillboardCloudVegetationPipeline = Simplygon::spBillboardCloudVegetationPipeline::SafeCast( LODPipeline );
		FStaticMeshSourceModel& SrcModel = StaticMesh->GetSourceModel(LODIndex);
		SrcModel.BuildSettings.bGenerateLightmapUVs = false;
		SrcModel.BuildSettings.bRecomputeNormals = false;
		SrcModel.BuildSettings.bRecomputeTangents = false;
		//Make sure the mesh do not get reduce if there was no mesh data before.
		//In this case we have a generated LOD convert to a LOD Recipe
		SrcModel.ReductionSettings.MaxDeviation = 0.0f;
		SrcModel.ReductionSettings.PercentTriangles = 1.0f;
		SrcModel.ReductionSettings.PercentVertices = 1.0f;
		if (BillboardCloudVegetationPipeline) 
		{
			SrcModel.BuildSettings.bGenerateLightmapUVs = true;
			SrcModel.BuildSettings.SrcLightmapIndex = 0;
			SrcModel.BuildSettings.DstLightmapIndex = StaticMesh->GetSourceModel(0).BuildSettings.DstLightmapIndex;
			StaticMesh->SetLightMapCoordinateIndex(SrcModel.BuildSettings.DstLightmapIndex);
			StaticMesh->SetLightmapUVVersion((int32)ELightmapUVVersion::Latest);
		}
		FMeshDescription* MeshDescription = StaticMesh->CreateMeshDescription(LODIndex);
		check(MeshDescription != nullptr);
		*MeshDescription = ReducedMeshDescriptions[LODIndex];

		
		
		//Add baked materials
		bool bHasBakedMaterials = PerLODBakedMaterials.Contains(LODIndex);
		if (bHasBakedMaterials)
		{
			FFlattenMaterial* BakedMaterials = PerLODBakedMaterials.Find(LODIndex);
			if(BakedMaterials)
				ApplyBakedMaterials(StaticMesh, LODIndex, *BakedMaterials, Pipeline, PerLODMaterialIndices[LODIndex], PerLODBakedUVSets[LODIndex]);
		}
		else
		{
			auto PolygonGroupMaterialSlotName = MeshDescription->PolygonGroupAttributes().GetAttributesRef<FName>(MeshAttribute::PolygonGroup::ImportedMaterialSlotName);
			
			// Build LOD section info using LOD 0 as source
			for (const FPolygonGroupID PolygonGroupID : MeshDescription->PolygonGroups().GetElementIDs())
			{
				const FName SlotName = PolygonGroupMaterialSlotName[PolygonGroupID];
				if (SlotNameToMaterialElement.Contains(SlotName) && LOD0SlotNameToSectionIndex.Contains(SlotName))
				{
					int32 LOD0SectionIndex = LOD0SlotNameToSectionIndex[SlotName];
					const FMeshSectionInfo& SectionInfo = StaticMesh->GetSectionInfoMap().Get(0, LOD0SectionIndex);
					StaticMesh->GetSectionInfoMap().Set(LODIndex, PolygonGroupID.GetValue(), SectionInfo);
				}
			}
		}

		//check difference in mesh description bounds
		FBoxSphereBounds MeshDescriptionBounds = MeshDescription->GetBounds();

		/*
		* Remeshing results can generate meshes which could be larger than the source bounds. This usually results
		* in flickering issues when running hardware occlusion queries. The Negative/PositiveBounds extension provide
		* users a mechanism to adjust these. These extended bounds are used during occlusion cull queries.
		*/
		//StaticMesh GetBounds / GetBounding box both internally call GetExtendedBounds.
		//And GetExtendedBounds when run from the editor will check for a CachedMeshDescription/CachedMeshDescriptionBounds
		//CachedMeshDescriptionBounds are set when CommitMeshDescription is called. 
		if (GetMutableDefault<USimplygonSettings>()->bAdjustLODBounds && FSimplygonDataConverter::IsPipelineOfType<Simplygon::spRemeshingPipeline>(LODPipeline))
		{
			FBoxSphereBounds StaticMeshBounds = StaticMesh->GetBounds();
			FVector NegativeBounds = (MeshDescriptionBounds.GetBox().Min - StaticMeshBounds.GetBox().Min).GetAbs();
			FVector PositiveBounds = (MeshDescriptionBounds.GetBox().Max - StaticMeshBounds.GetBox().Max).GetAbs();
			StaticMesh->SetNegativeBoundsExtension(NegativeBounds);
			StaticMesh->SetPositiveBoundsExtension(PositiveBounds);
		}
		
		StaticMesh->CommitMeshDescription(LODIndex);

		GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostLODImport(StaticMesh, LODIndex);
	}

	//Apply Streamed LODs
	ApplyStreamedLODs(StaticMesh, false);

	// Make sure it has a new lighting guid
	StaticMesh->SetLightingGuid();

	StaticMesh->PostEditChange();
	
	//Refresh static mesh editor if it is open
	UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
	IAssetEditorInstance* EditorInstance = AssetEditorSubsystem->FindEditorForAsset(StaticMesh, false);
	if (EditorInstance)
	{
		IStaticMeshEditor* StaticMeshEditor = static_cast<IStaticMeshEditor*>(EditorInstance);
		StaticMeshEditor->RefreshTool();
	}
}

void ULODRecipe::ProcessSkeletalMeshCompleted(USkeletalMesh* SkeletalMesh, const FSimplygonLODRecipeJobData& /*LODRecipeJobData*/, const Simplygon::spPipeline& Pipeline, const Simplygon::spScene& CascadedScene)
{
	check(IsInGameThread())

	FSkeletalMeshModel* SkeletalMeshResource = SkeletalMesh->GetImportedModel();
	check(SkeletalMeshResource);

	UE_LOG(LogLODRecipe, Display, TEXT("Simplygon pipeline complete, finalizing %s"), *SkeletalMesh->GetName());

	TArray<FSkeletalMeshLODModel*> ReducedSkeletalMeshLODModels;
	ReducedSkeletalMeshLODModels.SetNum(NumLODs);

	TMap<int32, FFlattenMaterial> PerLODBakedMaterials;
	PerLODBakedMaterials.Empty();

	TArray<bool> PerLODUseImportedLOD;
	PerLODUseImportedLOD.Init(false, NumLODs);

	auto CopyExistingLODModel = [&](const int32 SrcLODIndex, const int32 DestLODIndex)
	{
		ReducedSkeletalMeshLODModels[DestLODIndex] = new FSkeletalMeshLODModel();
		
		if (!SkeletalMeshResource->LODModels.IsValidIndex(SrcLODIndex))
			return false;
		
		FSkeletalMeshLODModel::CopyStructure(ReducedSkeletalMeshLODModels[DestLODIndex], &SkeletalMeshResource->LODModels[SrcLODIndex]);
		return true;
	};

	// Convert to SkeletalMeshLODModel
	for (int32 LODIndex = 0; LODIndex < NumLODs; ++LODIndex)
	{
		//SG-TODO: Allow LOD0 reductions? If so change LODIndex - 1 below
		if (LODIndex == 0)
			continue;

		Simplygon::spPipeline LODPipeline = Pipeline->GetCascadedPipelineByIndex(LODIndex - 1);

		if (PerLODMiscSettings[LODIndex].bUseImportedLOD &&
			SkeletalMesh->IsValidLODIndex(LODIndex) &&
			SkeletalMesh->GetLODInfo(LODIndex)->bHasBeenSimplified == false)
		{
			PerLODUseImportedLOD[LODIndex] = true;
			continue;
		}

		// Fall back to LOD 0 if a PassthroughPipeline is found.
		// See ULODRecipe::CreatePipeline for more info about the logic
		if (Simplygon::spPassthroughPipeline::SafeCast(LODPipeline))
		{
			CopyExistingLODModel(0, LODIndex);
			continue;
		}
		
		Simplygon::spScene ProcessedScene = LODPipeline->GetProcessedScene();
		if (ProcessedScene.IsNull())
		{
			ProcessedScene = CascadedScene->GetCascadedSceneForIndex(LODIndex - 1);
			if (ProcessedScene.IsNull())
			{
				UE_LOG(LogLODRecipe, Error, TEXT("LOD recipe processing failed for %s"), *SkeletalMesh->GetFName().ToString());
				PerLODMiscSettings[LODIndex].bUseImportedLOD = true;
				continue;
			}
		}

		int32 SelectionSetId = ProcessedScene->SelectNodes("ISceneMesh");
		Simplygon::spSelectionSet SelectedMeshNodes = ProcessedScene->GetSelectionSetTable()->GetSelectionSet(SelectionSetId);
		Simplygon::spSceneMesh ReducedMesh;
		if (SelectedMeshNodes->GetItemCount() > 0)
			ReducedMesh = Simplygon::spSceneMesh::SafeCast(ProcessedScene->GetNodeByGUID(SelectedMeshNodes->GetItem(0)));
		if (ReducedMesh.IsNull())
		{
			UE_LOG(LogLODRecipe, Error, TEXT("LOD recipe processing failed for %s"), *SkeletalMesh->GetFName().ToString());
			PerLODMiscSettings[LODIndex].bUseImportedLOD = true;
			continue;
		}

		// Convert to FlattenMaterial structure
		const bool bHasBakedMaterials = LODRecipeHelper::IsPipelineCastingMaterials(LODPipeline);
		if (bHasBakedMaterials)
		{
			FFlattenMaterial BakedMaterial;
			FSimplygonDataConverter::CreateFlattenMaterial(ProcessedScene, BakedMaterial);
			BakedMaterial.FillAlphaValues(255);
			PerLODBakedMaterials.Add(LODIndex, BakedMaterial);
		}

		// Create Geometry Data textures
		if (LODRecipeHelper::IsPipelineCastingGeometryData(LODPipeline))
		{
			TArray<UObject*> AssetsToSync;
			FSimplygonDataConverter::CreateGeometryDataTextures(ProcessedScene, LODPipeline, GetBakedMaterialName(SkeletalMesh, LODIndex), AssetsToSync);
			LODRecipeHelper::SyncAssets(AssetsToSync);
			NewAssetsCreatedEvent.Broadcast(&AssetsToSync);
		}

		ReducedSkeletalMeshLODModels[LODIndex] = new FSkeletalMeshLODModel();

		Simplygon::spGeometryData GeometryData = ReducedMesh->GetGeometry();
		const FLODRecipeMiscSettings& MiscSettings = PerLODMiscSettings[LODIndex];
		bool bApplyMorphTargetsToLOD = MiscSettings.bApplyMorphTargets;
		const auto* MorphTargetsToRemove = MiscSettings.GetMorphTargetsToRemove(SkeletalMesh);
		FBoxSphereBounds SkeletalMeshCurrentLODBounds;
		FSimplygonDataConverter::CreateSkeletalLODModelFromGeometry(
			ReducedSkeletalMeshLODModels[LODIndex], 
			GeometryData, 
			SkeletalMesh, 
			LODIndex, 
			bHasBakedMaterials,
			SkeletalMeshCurrentLODBounds,
			bApplyMorphTargetsToLOD,
			MorphTargetsToRemove);

		/*
		* Skeletal Mesh bounds use the imported bounds. For proxy meshes these need to adjusted.
		* Mainly since the processed mesh can be larger/intersect the bounds. 
		* This likely results in issues with hardware occlusion queries.
		* LODBounds are fetched form CreateSkeletalLODModelFromGeometry and diff against the bounds in the SkeletalMesh Object.
		* Any differences are added as Bounds Extension.
		*/
		if (GetMutableDefault<USimplygonSettings>()->bAdjustLODBounds 
			&& FSimplygonDataConverter::IsPipelineOfType<Simplygon::spRemeshingPipeline>(LODPipeline))
		{
			FBoxSphereBounds SkeletalMeshBounds = SkeletalMesh->GetBounds();
			FVector NegativeBounds = (SkeletalMeshCurrentLODBounds.GetBox().Min - SkeletalMeshBounds.GetBox().Min).GetAbs();
			FVector PositiveBounds = (SkeletalMeshCurrentLODBounds.GetBox().Max - SkeletalMeshBounds.GetBox().Max).GetAbs();
			SkeletalMesh->SetNegativeBoundsExtension(NegativeBounds);
			SkeletalMesh->SetPositiveBoundsExtension(PositiveBounds);
		}
	}

	FScopedSuspendAlternateSkinWeightPreview ScopedSuspendAlternateSkinnWeightPreview(SkeletalMesh);
	{
		FScopedSkeletalMeshPostEditChange ScopedPostEditChange(SkeletalMesh);

		// Unbind any existing clothing assets before we regenerate all LODs
		TArray<ClothingAssetUtils::FClothingAssetMeshBinding> ClothingBindings;
		FLODUtilities::UnbindClothingAndBackup(SkeletalMesh, ClothingBindings);

		// Invalidate ddc key to make sure we do not pickup old LOD data,
		// this is needed since we are not using reduction interface. 
		SkeletalMesh->InvalidateDeriveDataCacheGUID();

		FSkeletalMeshUpdateContext UpdateContext;
		UpdateContext.SkeletalMesh = SkeletalMesh;

		// Remove LODs
		int32 CurrentNumLODs = SkeletalMesh->GetLODNum();
		if (NumLODs < CurrentNumLODs)
		{
			for (int32 LODIdx = CurrentNumLODs - 1; LODIdx >= NumLODs; LODIdx--)
			{
				FLODUtilities::RemoveLOD(UpdateContext, LODIdx);
			}
		}		

		ResetMaterialsAndSectionInfo(SkeletalMesh, PerLODMiscSettings);

		/* Apply LODs*/
		for (int32 LODIndex = 0; LODIndex < NumLODs; LODIndex++)
		{
			//SG-TODO: Allow LOD0 reductions?
			if (LODIndex == 0)
				continue;

			if (PerLODMiscSettings[LODIndex].bUseImportedLOD)
				continue;

			FScopedSkeletalMeshPostEditChange ScopedPostEditChangeLOD(SkeletalMesh);

			check(LODIndex <= SkeletalMeshResource->LODModels.Num());

			int32 BaseLOD = 0;
			bool bNewLODSlot = false;

			// If the Current LOD is an import from file
			bool OldLodWasFromFile = SkeletalMesh->IsValidLODIndex(LODIndex) &&
				SkeletalMesh->GetLODInfo(LODIndex)->bHasBeenSimplified == false;

			// Insert a new LOD model entry if needed.
			if (LODIndex == SkeletalMeshResource->LODModels.Num())
			{
				SkeletalMeshResource->LODModels.Add(nullptr);
				bNewLODSlot = true;
			}

			if (LODIndex == SkeletalMesh->GetLODNum())
			{
				SkeletalMesh->AddLODInfo();
			}

			// Swap in a new model, delete the old.
			check(LODIndex < SkeletalMeshResource->LODModels.Num());
			FSkeletalMeshLODModel** MeshLODModels = SkeletalMeshResource->LODModels.GetData();
			FSkeletalMeshLODModel* OldLODModel = MeshLODModels[LODIndex];
			MeshLODModels[LODIndex] = ReducedSkeletalMeshLODModels[LODIndex];
			delete OldLODModel;

			if (OldLodWasFromFile)
			{
				SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap.Empty();
			}

			// If base lod has a customized LODMaterialMap and this LOD doesn't (could have if changes are applied
			// instead of freshly generated, copy over the data into new LOD
			if (SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap.Num() == 0 &&
				SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap.Num() != 0)
			{
				SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap =
					SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap;
			}
			else
			{
				// Assuming the reducing step has set all material indices correctly, we double check if something
				// went wrong make sure we don't have more materials
				int32 TotalSectionCount = MeshLODModels[LODIndex]->Sections.Num();
				if (SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap.Num() > TotalSectionCount)
				{
					SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap =
						SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap;
					// Something went wrong during the reduce step during regenerate
					check(SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap.Num() == TotalSectionCount);
				}
			}

			// Set to false to avoid calling FLODUtilities::RegenerateDependentLODs when caching skeletal mesh LOD 0
			SkeletalMesh->GetLODInfo(LODIndex)->bHasBeenSimplified = false;
			SkeletalMesh->SetHasBeenSimplified(true);

			//SG-TODO: Support native bone removal tool
			/*SkeletalMesh->CalculateRequiredBones(SkeletalMeshResource->LODModels[LODIndex],
				SkeletalMesh->RefSkeleton, &BonesToRemove);*/

			FSkeletalMeshLODInfo* NewLODInfo = SkeletalMesh->GetLODInfo(LODIndex);
			NewLODInfo->ReductionSettings.NumOfTrianglesPercentage = 1.0f;
			NewLODInfo->ReductionSettings.NumOfVertPercentage = 1.0f;
			NewLODInfo->ReductionSettings.MaxDeviationPercentage = 0.0f;
			NewLODInfo->LODHysteresis = 0.02f;

			//Add baked materials
			bool bHasBakedMaterials = PerLODBakedMaterials.Contains(LODIndex);
			if (bHasBakedMaterials)
			{
				FFlattenMaterial* BakedMaterials = PerLODBakedMaterials.Find(LODIndex);
				if(BakedMaterials)
					ApplyBakedMaterials(SkeletalMesh, LODIndex, *BakedMaterials, Pipeline);
			}

			//This will make sure AssetDetails tab gets refreshed with new LOD data
			GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostLODImport(SkeletalMesh, LODIndex);
		}
		/*End Apply LODs*/

		//Restore all clothing we can
		FLODUtilities::RestoreClothingFromBackup(SkeletalMesh, ClothingBindings);

		SkeletalMesh->CalculateInvRefMatrices();
	}
}

void ULODRecipe::SetupPerLODMaterialIds( Simplygon::spScene& Scene,
                                     TMap<int32, FName>& MaterialNameMap,
                                     Simplygon::spSceneMesh& ReducedMesh,
                                     TArray<int32>& OutSectionIndices )
{
	auto Geometry = ReducedMesh->GetGeometry();
	SetupPerLODMaterialIds( Scene, MaterialNameMap, Geometry, OutSectionIndices );
}

void ULODRecipe::SetupPerLODMaterialIds( Simplygon::spScene& Scene,
                                     TMap<int32, FName>& MaterialNameMap,
                                     Simplygon::spGeometryData& ProcessedGeometry,
                                     TArray<int32>& OutSectionIndices )
{
	OutSectionIndices.Empty();

	for( auto MaterialIndex = 0U; MaterialIndex < Scene->GetMaterialTable()->GetItemsCount(); ++MaterialIndex )
	{
		Simplygon::spMaterial MaterialTemp = Scene->GetMaterialTable()->GetMaterial( MaterialIndex );
		FString MaterialName = ANSI_TO_TCHAR( MaterialTemp->GetName() );
		MaterialNameMap.Add( MaterialIndex, FName( MaterialName ) );
	}

	// Get unique material ids from GeometryData
	if( !ProcessedGeometry.IsNull() && !ProcessedGeometry->GetMaterialIds().IsNull() )
	{
		auto MaterialIdsData = ProcessedGeometry->GetMaterialIds()->GetData();

		for( auto Index = 0U; Index < MaterialIdsData.GetItemCount(); ++Index )
		{
			OutSectionIndices.AddUnique( MaterialIdsData.GetItem( Index ) );
		}
	}
}

void ULODRecipe::ApplyStreamedLODsToMeshes(bool bPostEditChange)
{
	// Apply to static meshes
	for (TSoftObjectPtr<UStaticMesh> SoftStaticMesh : StaticMeshes)
	{
		UStaticMesh* StaticMesh = SoftStaticMesh.LoadSynchronous();
		ApplyStreamedLODs(StaticMesh, bPostEditChange);
	}

	// Apply to skeletal meshes
	for (TSoftObjectPtr<USkeletalMesh> SoftSkeletalMesh : SkeletalMeshes)
	{
		USkeletalMesh* SkeletalMesh = SoftSkeletalMesh.LoadSynchronous();
		ApplyStreamedLODs(SkeletalMesh, bPostEditChange);
	}
}

void ULODRecipe::ApplyStreamedLODs(UStaticMesh* StaticMesh, bool bPostEditChange)
{
	bool bHasNewStreamedLODsValue = true;
	StaticMesh->Modify();
	{
		FStaticMeshComponentRecreateRenderStateContext ReregisterContext(StaticMesh, false);
		if (StaticMesh->NumStreamedLODs.Default != NumStreamedLODs.Default)
		{
			bHasNewStreamedLODsValue = true;
			StaticMesh->NumStreamedLODs.Default = NumStreamedLODs.Default;
		}

		bool bIsPerPlatformSame = StaticMesh->NumStreamedLODs.PerPlatform.OrderIndependentCompareEqual(NumStreamedLODs.PerPlatform);
		if (!bIsPerPlatformSame)
		{
			bHasNewStreamedLODsValue = true;
			StaticMesh->NumStreamedLODs.PerPlatform.Empty(NumStreamedLODs.PerPlatform.Num());
			StaticMesh->NumStreamedLODs.PerPlatform = NumStreamedLODs.PerPlatform;
		}
	}

	if (bPostEditChange && bHasNewStreamedLODsValue)
	{
		IStreamingManager::Get().GetTextureStreamingManager().BlockTillAllRequestsFinished();
		StaticMesh->PostEditChange();
	}
}

void ULODRecipe::ApplyStreamedLODs(USkeletalMesh* SkeletalMesh, bool /*bPostEditChange*/)
{
	FPerPlatformBool SupportLODStreaming = SkeletalMesh->GetSupportLODStreaming();

	//Enable LOD streaming support
	SupportLODStreaming.Default = NumStreamedLODs.Default != -1;

	SupportLODStreaming.PerPlatform.Empty(NumStreamedLODs.PerPlatform.Num());
	for (auto PlatformKvp : NumStreamedLODs.PerPlatform)
	{
		if (PlatformKvp.Value == -1)
			continue;

		SupportLODStreaming.PerPlatform.Add(PlatformKvp.Key, true);
	}

	SkeletalMesh->SetSupportLODStreaming(SupportLODStreaming);

	//Apply LODs to stream
	SkeletalMesh->SetMaxNumStreamedLODs(NumStreamedLODs);
}

#endif

#undef LOCTEXT_NAMESPACE
